package geom;


import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.lang.reflect.Field;

import seg.appx.Fit;
import stat.Matrix;
import doc.SampledPoint;


public class Ellipse implements Fit {

	private Point[] points;

	private SampledPoint[] origin;

	public void setInk(SampledPoint[] ps) {
		origin = ps;
	}

	public SampledPoint[] getInk() {
		return origin;
	}

	private Point center;

	private double major;

	private double minor;

	private double angle;

	public Point getCenter() {
		return center;
	}

	public void setCenter(Point center) {
		this.center = center;
	}

	public double getMajor() {
		return major;
	}

	public void setMajor(double major) {
		this.major = major;
	}

	public double getMinor() {
		return minor;
	}

	public void setMinor(double minor) {
		this.minor = minor;
	}

	public double getAngle() {
		return angle;
	}

	public void setAngle(double angle) {
		this.angle = angle;
	}

	public Ellipse() {

	}

	public void print() {
		System.out.println("Ellipse ========================================");
		Field[] fs = this.getClass().getDeclaredFields();
		for (int i = 0; i < fs.length; i++) {
			try {
				if (fs[i].getName().equals("points"))
					continue;
				System.out.println(fs[i].getName() + "=" + fs[i].get(this).toString());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		System.out.println("================================================");
	}

	public void draw(Graphics2D g2) {
		draw(g2, Color.RED, 2);
//		drawMine(g2, Color.BLACK);
	}

	public void drawMine(Graphics2D g2, Color c) {
		double cur = 0;
		double inc = Math.PI / 16;
		Point p = getPointAt(cur);
		g2.setColor(c);
		while (cur < 2*Math.PI) {
			cur += inc;
			Point n = getPointAt(cur);
			g2.drawLine(p.intX(), p.intY(), n.intX(), n.intY());
			p = n;
		}
	}

	public void draw(Graphics2D g2, Color c, int w) {
		g2.setStroke(new BasicStroke(w));
//		Point[] ps = getPoints();
//		if (ps == null || ps.length < 1)
//			return;
//		Point p = ps[0];
		g2.setColor(Color.RED);
//		for (int i = 1; i< ps.length; i++) {
//			g2.drawLine(p.intX(), p.intY(), ps[i].intX(), ps[i].intY());
//			p = ps[i];
//		}
//		g2.drawLine(ps[X].intX(), ps[X].intY(), ps[Y].intX(), ps[Y].intY());
		double cur = 0;
		double inc = Math.PI / 32;
		Point p = getPointAt(cur);
		for (cur = inc; Math.abs(cur - 2 * Math.PI) > .5 * inc; cur += inc) {
			Point n = getPointAt(cur);
//			System.out.println(n);
			g2.drawLine(p.intX(), p.intY(), n.intX(), n.intY());
			p = n;
		}
	}

	public Point getPointAt(double theta) {
		Matrix m = new Matrix(2,2);
		double cos = Math.cos(-getAngle());
		double sin = Math.sin(-getAngle());
		m.setValues(new double[][] {
				new double[] { cos, sin },
				new double[] { -sin, cos }
		});
		Matrix op = new Matrix(2,1);
		op.setValues(new double[][] {
				new double[] { getMajor() * Math.cos(theta) },
				new double[] { getMinor() * Math.sin(theta) }
		});

		Matrix np1 = m.multiply(op);
		return new Point(np1.valueAt(0, 0) + getCenter().getX(),
						 np1.valueAt(1, 0) + getCenter().getY());
	}

	public Point[] getPoints() {
		return points;
	}

	public void setPoints(Point[] points) {
		this.points = points;
	}

	public int calcSignOf(Point sp, Point mp, Point ep) {

		double as = getNearestPointAngle(sp);
		double ae = getNearestPointAngle(ep);
		double an = getNearestPointAngle(mp);

		int sign;
		if (ae > as) {
			if (an > as && an < ae)
				// as < an < ae
				sign = 1;
			else
				sign = -1;
		} else {
			if (an > ae && an < as)
				// as > an > ae
				sign = -1;
			else
				sign = 1;
		}

		return sign;
	}

	public static Ellipse ofBound(Point[] ps) {
		// calc bound of an ellipse sampled points
		int X = 0, Y = 0;
		double max = 0;
		for (int i = 0; i < ps.length; i++) {
			for (int j = i + 1; j < ps.length; j++) {
				double dis = ps[i].distance(ps[j]);

				if (dis > max) {
					max = dis;
					X = i;
					Y = j;
				}
			}
		}

		Ellipse el = new Ellipse();
		el.setMajor(.5 * max);

		Line line = new Line(ps[X], ps[Y]);
		for (int i = X + 1; i != X; i = (i+1) % ps.length) {
			double dis = line.ptLineDist(ps[i]);
			if (dis > el.getMinor())
				el.setMinor(dis);
		}

		el.setAngle(Math.atan( (ps[Y].getY() - ps[X].getY())
							 / (ps[Y].getX() - ps[X].getX()) ));
		el.setCenter(line.getCenter());
		return el;
	}

	public double ptEllipseDist(Point p) {
//		Matrix m = new Matrix(2,2);
//		double cos = Math.cos(getAngle());
//		double sin = Math.sin(getAngle());
//		m.setValues(new double[][] {
//				new double[] { cos, sin },
//				new double[] { -sin, cos }
//		});
//
//		Matrix mp = new Matrix(2,1);
//		mp.setValueAt(0, 0, p.getX());
//		mp.setValueAt(1, 0, p.getY());
//
//		mp = m.multiply(mp);
//		Point np = new Point(Math.abs(mp.valueAt(0, 0) - getCenter().getX()), Math.abs(mp.valueAt(1, 0) - getCenter().getY()));
		// make the point in first coordinate
//		System.out.println("new point = " + np);

//		double inc = getMajor() / 64;
//		double cur = 0;
//		double min = Double.MAX_VALUE;
//		double A = getMajor(), B = getMinor(), AA = A * A;
//		Point pos = null;

		double cur = 0;
		double inc = Math.PI / 64;
//		double eps = .5 * inc;
		double min = Double.MAX_VALUE;
		for ( ; cur < Math.PI * 2; cur += inc) {
			Point n = getPointAt(cur);
			double dis2 = n.distanceSq(p);
			if (min > dis2)
				min = dis2;
		}
		return Math.sqrt(min);
	}

	public double avgPtEllipseDist(SampledPoint[] ps) {
		if (ps == null || ps.length == 0) {
			return 0;
		}

		double sum = 0;
		for (SampledPoint p : ps) {
			double dis = ptEllipseDist(p.toPoint());
//			System.err.println(this.toString() + p.toString() + " = " + dis);
			sum += dis;
		}
		return sum / ps.length;
	}

	public String toString() {
		return "- Ellipse -\n  center=" + getCenter().toString()
		+ "\n  major=" + getMajor() + "\n  minor=" + getMinor()
		+ "\n  theta=" + getAngle() + "\n";
	}

	public double avgPtEllipseDist(Point[] ps) {
		if (ps == null || ps.length == 0)
			return 0;

		double sum = 0;
		for (Point p : ps) {
			sum += ptEllipseDist(p);
		}
		return sum / ps.length;
	}

	public static void main(String[] args) {
		Ellipse el = new Ellipse();
		el.setCenter(new Point(0,0));
		el.setAngle(.25*Math.PI);
		el.setMajor(3);
		el.setMinor(2);
		Point p = new Point(Math.sqrt(2), Math.sqrt(2));

		System.out.println(el.ptEllipseDist(p));
	}

	public void drawFit3(Graphics2D g2) {
		draw(g2, Color.RED, 2);
	}

	public void drawFit(Graphics2D g2) {
		if (getInk().length < 3)
			return;

		Point sp = getInk()[0].toPoint();
		Point ep = getInk()[getInk().length-1].toPoint();
		Point spn = getInk()[getInk().length/2].toPoint();

		int sign = calcSignOf(sp, spn, ep);

		double ae = getNearestPointAngle(ep);
		double as = getNearestPointAngle(sp);

		double cur = as;
		double inc = sign * Math.PI / 16;
		Point p = getPointAt(cur);

		g2.setColor(Color.RED);
		g2.setStroke(new BasicStroke(2));

		while (Math.abs(ae - cur) > Math.abs(.5 * inc)) {
			cur = inc + cur;
			if (cur > 2 * Math.PI)
				cur = cur - 2 * Math.PI;
			if (cur < 0)
				cur = cur + 2 * Math.PI;
			Point n = getPointAt(cur);
			g2.drawLine(p.intX(), p.intY(), n.intX(), n.intY());
			p = n;
		}
	}

	private double getNearestPointAngle(Point p) {
		double min = Double.MAX_VALUE;
		double angle = 0;
		for (double cur = 0; cur < 2*Math.PI; cur += Math.PI/32) {
			Point n = getPointAt(cur);
			double dis = n.distanceSq(p);
			if (min > dis) {
				min = dis;
				angle = cur;
			}
		}
		return angle;
	}

	private int getNearestPointIndex(Point p) {
		double min = Double.MAX_VALUE;
		int index = 0;
		for (int i = 0; i < getPoints().length; i++) {
			double dis = getPoints()[i].distance(p);
			if (min > dis) {
				min = dis;
				index = i;
			}
		}
		return index;
	}

	public void drawFit2(Graphics2D g2) {
		if (getInk().length < 3)
			return;

		Point sp = getInk()[0].toPoint();
		Point ep = getInk()[getInk().length-1].toPoint();
		Point spn = getInk()[getInk().length/2].toPoint();

		int is = getNearestPointIndex(sp);
		int ie = getNearestPointIndex(ep);
		int in = getNearestPointIndex(spn);
		int sign = -1;
		if (totalGreater(in, is) || totalGreater(ie, in))
			sign = 1;
//		System.out.println("s=" + is + ", e=" + ie + "sign=" + sign);
		g2.setColor(Color.RED);
		g2.setStroke(new BasicStroke(2));
		Point prev = getPoints()[is];
		int i = is;
		for (i = nextIndex(i, sign); i != ie; i = nextIndex(i, sign)) {
//			System.out.println("index=" + i);
			Point next = getPoints()[i];
			g2.drawLine(prev.intX(), prev.intY(), next.intX(), next.intY());
			prev = next;
		}
	}

	private boolean totalGreater(double a, double b) {
		return a > b && Math.abs(a) > Math.abs(b);
	}

	private int nextIndex(int i, int sign) {
		return  (i + sign * 1 + getPoints().length) % getPoints().length;
	}

	public void setInk(SampledPoint[] points, int s, int e) {
		SampledPoint[] ps = new SampledPoint[e-s];
		for (int i = 0; i < ps.length; i++) {
			ps[i] = points[i+s];
		}
		setInk(ps);
	}

	public boolean isEllipse() {
		return true;
	}

	public boolean isLine() {
		return false;
	}
}
